/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package de.blitzcoder.collide.engine;

import de.blitzcoder.collide.Interface;
import de.blitzcoder.collide.gui.Document;
import de.blitzcoder.collide.gui.OutputTextArea;
import de.blitzcoder.collide.gui.debugger.BlitzMaxDebugger;
import de.blitzcoder.collide.util.Log;
import de.blitzcoder.collide.util.OS;
import de.blitzcoder.collide.util.StringWorker;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.LinkedList;
import java.util.ListIterator;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JToolBar;
import javax.swing.SwingUtilities;

/**
 *
 * @author blitzcoder
 */
public class RunningProcess extends Document implements Runnable, OutputTextArea.StdInListener {

    // Properties
    private Process process;
    private String[] command; // Needed to kill the Process on Windows Systems
    private String name;
    private boolean isBMax = false; // Debugger only is enabled on BlitzMax-Processes
    private boolean processRunning = false;

    // Components
    private OutputTextArea textArea;
    private BlitzMaxDebugger debugger = null;
    private JSplitPane splitPane;
    private JToolBar toolBar;

    // Cache
    private String debugOutputBuffer = ""; // Caches long debug output

    // Constants
    private static final int INTERVAL = 100; // Interval in ms for looking for new output on stdout/stderr

    /*
     * Constructors
     */
    public RunningProcess(String name, String[] cmd) throws IOException {
        
        Process process = Runtime.getRuntime().exec(cmd);

        this.command = cmd;
        this.name = name;
        this.process = process;

        processRunning = true;

        final Process p = process;
        new Thread("ProcessWaiter") {

            public void run() {
                try {
                    p.waitFor();
                } catch (Exception ex) {
                    throw new Error(ex);
                }
                processRunning = false;
                fireDocumentUpdate();
            }
        }.start();

        // Close all finished Processes
        Document[] docs = Interface.getOutput().getAllDocuments();
        for (Document doc:docs) {
            if (doc instanceof RunningProcess) {
                RunningProcess runProc = (RunningProcess)doc;
                if (!runProc.processRunning) {
                    Interface.getOutput().removeDocument(doc);
                }
            }
        }

    }

    @Override
    public void load() {
        setupUI();
        setupThread();
        printProcessInfo();

        textArea.addStdInListener(this);
    }

    public void setBlitzMax(boolean bm) {
        this.isBMax = bm;
    }

    @Override
    public boolean close() {
        if (this.processRunning) {
            
            int retVal = JOptionPane.showConfirmDialog(Interface.get(), this.name+" läuft noch. Soll er wirklich beendet werden?");
            switch (retVal) {
                case JOptionPane.OK_OPTION:
                    killProcess();
                    return true;
                default:
                    return false;
            }
            
        } else {
            return true;
        }
    }

    private void printProcessInfo() {
        String str = "";

        str+="----\n";
        str+="Binary: "+command[0]+"\n";
        if (command.length>1) {
            str+="Parameters: ";
            for (int i=1;i<command.length;i++)
                str+=command[i]+" ";
            str+="\n";
        } else {
            str+="(No Parameters)\n";
        }
        str+="----\n";

        textArea.append(str);
    }

    private void setupToolBar() {
        JButton kill = new JButton();
        kill.setIcon(de.blitzcoder.collide.icons.Icon.load("process_kill.png"));
        kill.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                killProcess();
            }
        });

        toolBar.add(kill);
    }

    public void setTextAreaFont(Font f) {
        textArea.setFont(f);
    }

    /*
     * Set Up the user interface
     */
    private void setupUI() {

        setLayout(new GridBagLayout());

        toolBar = new JToolBar();
        toolBar.setFloatable(false);

        textArea = new OutputTextArea();

        splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
        splitPane.setTopComponent(new JScrollPane(textArea));

        GridBagConstraints c = new GridBagConstraints();
        c.fill = c.BOTH;
        c.gridy = 0;
        c.weightx = 1.0;
        c.weighty = 0.0;
        add(toolBar,c);

        c.gridy = 1;
        c.weighty = 1.0;

        add(splitPane,c);

        setupToolBar();



    }

    /*
     * Set up the listener thread
     */
    private void setupThread() {

        Thread t = new Thread(this);
        t.start();

    }

    /*
     * Document methods
     */
    @Override
    public String getTitle() {
        if (processRunning)
            return "<html><b>"+name+" [running]</b></html>";
        else
            return name+" [stopped]";
    }

    @Override
    public Icon getIcon() {
        return null;
    }

    @Override
    public String getToolTip() {
        return name;
    }

    /*
     * A running Process never equals a file
     */
    @Override
    public boolean equalsFile(File file) {
        return false;
    }

    /*
     * Runnable Interface, Monitors for stdout/stderr and appends the stuff to
     * the textArea
     */
    @Override
    public void run() {

        InputStream input = process.getInputStream();
        InputStream error = process.getErrorStream();

        int avail = 0;

        for (;;) {

            // If process is dead, the rest of the streams should be read
            try {
                if (!processRunning && input.available() == 0 && error.available() == 0) {
                    appendTextToTextArea("\n---\nProzess wurde beendet");
                    textArea.setStdInEnabled(false);
                    return;
                }
            } catch (IOException ex) {
                // If process is dead, this should be a "Stream closed".
                // Display the "process terminated"-message in this case
                if (processRunning) {
                    throw new Error(ex);
                } else {
                    appendTextToTextArea("\n---\nProzess wurde beendet");
                    textArea.setStdInEnabled(false);
                    return;
                }
            }

            // Process is running
            try {

                // Read stdout
                String stdout = "";
                avail = input.available();
                for (int i = 0; i < avail; i++) {
                    stdout += (char) input.read();
                }

                // Read stderr
                String stderr = "";
                avail = error.available();
                for (int i = 0; i < avail; i++) {
                    stderr += (char) error.read();
                }

                if (isBMax) {
                    parseDebugOutput(stderr);
                }

                // Append stdout to textarea
                if (stdout.length() != 0) {
                    appendTextToTextArea(stdout);
                }

                // Append stderr if no debug messages are given
                if (stderr.length() != 0) {
                    if (!isBMax || stderr.indexOf("~>") == -1) {
                        appendTextToTextArea(stderr, true);
                    }
                }

            } catch (IOException ioex) {
                
            }

            try {
                Thread.sleep(INTERVAL);
            } catch (InterruptedException ex) {
                throw new Error(ex);
            }

        }

    }

    public void appendTextToTextArea(final String msg) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                appendTextToTextArea(msg, false);
            }
        });
    }

    private void appendTextToTextArea(String msg, boolean isErr) {
        SwingUtilities.invokeLater(new Appender(msg, isErr));
    }

    @Override
    public void stdIn(byte[] buf) {

        if (processRunning) {

            try {
                process.getOutputStream().write(buf);
                process.getOutputStream().flush();
            } catch (Exception ex) {
                throw new Error(ex);
            }

        }

    }

    private class Appender implements Runnable {

        boolean isStdErr;
        String txt;
        public Appender(String txt, boolean isStdErr) {
            this.txt = txt;
            this.isStdErr = isStdErr;

        }

        public void run() {

            if (txt.length() == 0) {
                return;
            }

            int start = 0;
            if (isStdErr) {
                start = textArea.getText().length();
            }

            textArea.append(txt);

            if (isStdErr) {
                textArea.addStdErrorRegion(start, textArea.getText().length() - 1);
            }
        }
    }

    protected void parseDebugOutput(String out) {

        out = out.replace("\r", "");

        if (out.indexOf("~>") != -1) {

            // Der Puffer wird nach out geworfen und es wird nach output gesucht.
            // Wird nichts gefunden, wandert der output am ende dieser methode
            // wieder in den Buffer
            out = debugOutputBuffer + out;
            debugOutputBuffer = "";

            if (debugger == null) {
                debugger = new BlitzMaxDebugger(this);
                splitPane.setBottomComponent(debugger);
            }
        }

        // Checks if the output contains "~>}"
        // an object dump might be not have finished
        boolean fullDump = out.indexOf("~>}") != -1;

        if (fullDump) {
            // Detect StackTrace
            if (out.indexOf(">StackTrace{") != -1) {
                debugger.parseStackTrace(out);
                return;
            }
            // Detect Object Dumps
            if (out.indexOf(">ObjectDump") != -1) {
                debugger.parseObjectDump(out);
                return;
            }
        }

        // Detect DebugStop and feed Debugger
        if (out.indexOf(">Debug") != -1) {
            Log.log("Detected DebugStop");
            // Send t to get StackTrace
            try {
                writeDebugger(116); // Write a 't'
                Log.log("Writing StackTrace");
            } catch (Exception ex) {
                ex.printStackTrace();
            } finally {
                return;
            }
        }

        // Detect UnhandledException and feed Debugger
        int pos = out.indexOf(">Unhandled Exception");
        if (pos != -1) {
            Log.log("Detected Unhandled Exception");

            JOptionPane.showMessageDialog(Interface.get(),
                    StringWorker.cutLine(out, pos));

            // Send t to get StackTrace
            try {
                writeDebugger(116); // Write a 't'
            } catch (Exception ex) {
                ex.printStackTrace();
            } finally {
                return;
            }
        }

        // Compile-Error
        if (out.indexOf("Compile Error") != -1) {
            BlitzMaxCompiler.parseCompileError(out);
            Log.log("error");
            return;
        }

        // If nothing has been found,
        debugOutputBuffer = out;

    }

    /*
     * Kill the process. Crashed processes often stay alive, so some
     * dirty workarounds are needed to kill them
     */
    public void killProcess() {
        if (this.process != null) {


            int pid = -1;
            if (!OS.isWindows()) {
                pid = getPID();
            }

            // try to kill the normal way
            this.process.destroy();

            // Kill on Linux/OSX if bmk doesnt manage to..
            if (!OS.isWindows()) {
                try {
                    // Kill the bmk process
                    Runtime.getRuntime().exec("/bin/kill " + pid);
                    Log.log("Killing PID #"+pid);
                    // Kill the actual bmax-binary
                    //String name = new File(command[0]).getName();
                    //Runtime.getRuntime().exec(new String[] {"killall","-9",});
                } catch (IOException ex) {
                    throw new Error(ex);
                }
            }

            // Kill on windows
            if (OS.isWindows()) {

                /*
                 * This is very dirty. Every Process with this name is being killed.
                 * java.lang.ProcessImpl created Processes on Windows, but there
                 * is no way to get the PID.
                 * TODO: Find a solution!
                 */
                try {
                    Runtime.getRuntime().exec(new String[]{"taskkill", "/IM", new File(command[0]).getName(), "/f"});
                } catch (Exception ex) {
                    throw new Error(ex);
                }
            }

            ListIterator<KillListener> it = killListenerList.listIterator();
            while (it.hasNext())
                it.next().processKilled();

        }
    }

    /*
     * A kill listener is called after the process has MANUALLY been killed
     */
    private LinkedList<KillListener> killListenerList = new LinkedList<KillListener>();
    public interface KillListener {
        public void processKilled();
    }
    public void addKillListener(KillListener l) {
        killListenerList.addLast(l);
    }


    /*
     * Get the pid of a UNIXProcess. Dirty Workaround via Reflection
     */
    public int getPID() {
        if (!OS.isLinux() && !OS.isMacOSX()) {
            throw new Error("Getting the pid is only supported on UNIX Platforms");
        }
        try {
            if (process.getClass().getName().equals("java.lang.UNIXProcess")) {
                Field f = process.getClass().getDeclaredField("pid");
                f.setAccessible(true);
                return f.getInt(process);
            }
            return -1;
        } catch (Exception ex) {
            return -1;
        }
    }

    public void writeDebugger(int[] ascii) {
        try {
            for (int i:ascii)
                process.getOutputStream().write(i);
            process.getOutputStream().write(13); // Carrige-Return
            process.getOutputStream().write(10); // Write Line-Feed
            process.getOutputStream().flush();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    public void writeDebugger(int ascii) {
        writeDebugger(new int[] {ascii});
    }
}
